package com.pivotal.cloud.security.password;

import com.pivotal.cloud.crypto.codec.Utf8;
import com.pivotal.cloud.crypto.keygen.BytesKeyGenerator;
import com.pivotal.cloud.crypto.keygen.KeyGenerators;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.crypto.generators.SCrypt;

import java.security.MessageDigest;
import java.util.Base64;

/**
 * @className: SCryptPasswordEncoder
 * @projectName: PivotalCloud项目
 * @module: PivotalCloud项目-SCryptPasswordEncoder类，主要位于Security安全模块-SCrypt模式密码加密模块
 * @content: SCryptPasswordEncoder-SCrypt模式密码加密
 * @author: Powered by Marklin
 * @datetime: 2025-06-24 13:21
 * @version: 1.0.0
 * @copyright: Copyright © 2018-2025 PivotalCloud Systems Incorporated. All rights
 * reserved.
 */
public class SCryptPasswordEncoder implements PasswordEncoder {

	private static final int DEFAULT_CPU_COST = 65536;

	private static final int DEFAULT_MEMORY_COST = 8;

	private static final int DEFAULT_PARALLELISM = 1;

	private static final int DEFAULT_KEY_LENGTH = 32;

	private static final int DEFAULT_SALT_LENGTH = 16;

	/**
	 * 实例化对象
	 */
	private static final PasswordEncoder INSTANCE = SCryptPasswordEncoder.newInstance();

	private final Log logger = LogFactory.getLog(getClass());

	private final int cpuCost;

	private final int memoryCost;

	private final int parallelization;

	private final int keyLength;

	private final BytesKeyGenerator saltGenerator;

	public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) {
		if (cpuCost <= 1) {
			throw new IllegalArgumentException("Cpu cost parameter must be > 1.");
		}
		if (memoryCost == 1 && cpuCost > 65536) {
			throw new IllegalArgumentException("Cpu cost parameter must be > 1 and < 65536.");
		}
		if (memoryCost < 1) {
			throw new IllegalArgumentException("Memory cost must be >= 1.");
		}
		int maxParallel = Integer.MAX_VALUE / (128 * memoryCost * 8);
		if (parallelization < 1 || parallelization > maxParallel) {
			throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel
					+ " (based on block size r of " + memoryCost + ")");
		}
		if (keyLength < 1 || keyLength > Integer.MAX_VALUE) {
			throw new IllegalArgumentException("Key length must be >= 1 and <= " + Integer.MAX_VALUE);
		}
		if (saltLength < 1 || saltLength > Integer.MAX_VALUE) {
			throw new IllegalArgumentException("Salt length must be >= 1 and <= " + Integer.MAX_VALUE);
		}
		this.cpuCost = cpuCost;
		this.memoryCost = memoryCost;
		this.parallelization = parallelization;
		this.keyLength = keyLength;
		this.saltGenerator = KeyGenerators.secureRandom(saltLength);
	}

	public static SCryptPasswordEncoder defaultPasswordEncoder() {
		return new SCryptPasswordEncoder(DEFAULT_CPU_COST, DEFAULT_MEMORY_COST, DEFAULT_PARALLELISM, DEFAULT_KEY_LENGTH,
				DEFAULT_SALT_LENGTH);
	}

	/**
	 * 获取实例
	 * @return 返回结果
	 */
	public static PasswordEncoder getInstance() {
		return INSTANCE;
	}

	/**
	 * 实例化对象
	 * @return 返回结果
	 */
	public static PasswordEncoder newInstance() {
		return defaultPasswordEncoder();
	}

	/**
	 * 密码加密
	 * @param rawPassword 原始密码
	 * @return 返回结果
	 */
	@Override
	public String encode(CharSequence rawPassword) {
		return digest(rawPassword, this.saltGenerator.generateKey());
	}

	/**
	 * 验证密码
	 * @param rawPassword 原始密码
	 * @param encodedPassword 加密密码
	 * @return 返回结果
	 */
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		if (encodedPassword == null || encodedPassword.length() < this.keyLength) {
			this.logger.warn("Empty encoded password");
			return false;
		}
		return decodeAndCheckMatches(rawPassword, encodedPassword);
	}

	/**
	 * 更新密码编码
	 * @param encodedPassword 加密密码
	 * @return 返回结果
	 */
	@Override
	public boolean upgradeEncoding(String encodedPassword) {
		if (encodedPassword == null || encodedPassword.isEmpty()) {
			return false;
		}
		String[] parts = encodedPassword.split("\\$");
		if (parts.length != 4) {
			throw new IllegalArgumentException("Encoded password does not look like SCrypt: " + encodedPassword);
		}
		long params = Long.parseLong(parts[1], 16);
		int cpuCost = (int) Math.pow(2, params >> 16 & 0xffff);
		int memoryCost = (int) params >> 8 & 0xff;
		int parallelization = (int) params & 0xff;
		return cpuCost < this.cpuCost || memoryCost < this.memoryCost || parallelization < this.parallelization;
	}

	private boolean decodeAndCheckMatches(CharSequence rawPassword, String encodedPassword) {
		String[] parts = encodedPassword.split("\\$");
		if (parts.length != 4) {
			return false;
		}
		long params = Long.parseLong(parts[1], 16);
		byte[] salt = decodePart(parts[2]);
		byte[] derived = decodePart(parts[3]);
		int cpuCost = (int) Math.pow(2, params >> 16 & 0xffff);
		int memoryCost = (int) params >> 8 & 0xff;
		int parallelization = (int) params & 0xff;
		byte[] generated = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization,
				this.keyLength);
		return MessageDigest.isEqual(derived, generated);
	}

	private String digest(CharSequence rawPassword, byte[] salt) {
		byte[] derived = SCrypt.generate(Utf8.encode(rawPassword), salt, this.cpuCost, this.memoryCost,
				this.parallelization, this.keyLength);
		String params = Long.toString(
				((int) (Math.log(this.cpuCost) / Math.log(2)) << 16L) | this.memoryCost << 8 | this.parallelization,
				16);
		StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2);
		sb.append("$").append(params).append('$');
		sb.append(encodePart(salt)).append('$');
		sb.append(encodePart(derived));
		return sb.toString();
	}

	private byte[] decodePart(String part) {
		return Base64.getDecoder().decode(Utf8.encode(part));
	}

	private String encodePart(byte[] part) {
		return Utf8.decode(Base64.getEncoder().encode(part));
	}

}
